{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a href=\"https://colab.research.google.com/github/jeshraghian/snntorch/blob/master/examples/tutorial_regression_1.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "id": "RBNKFLJIobYt"
   },
   "source": [
    "[<img src='https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/snntorch_alpha_w.png?raw=true' width=\"400\">](https://github.com/jeshraghian/snntorch/)\n",
    "\n",
    "# Regression with SNNs: Part I\n",
    "## Learning Membrane Potentials with LIF Neurons\n",
    "## By Alexander Henkes (https://orcid.org/0000-0003-4615-9271) and Jason K. Eshraghian (www.ncg.ucsc.edu)\n",
    "\n",
    "\n",
    "<a href=\"https://colab.research.google.com/github/jeshraghian/snntorch/blob/master/examples/tutorial_regression_1.ipynb\">\n",
    "  <img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/>\n",
    "</a>\n",
    "\n",
    "[<img src='https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/GitHub-Mark-Light-120px-plus.png?raw=true' width=\"28\">](https://github.com/jeshraghian/snntorch/) [<img src='https://github.com/jeshraghian/snntorch/blob/master/docs/_static/img/GitHub_Logo_White.png?raw=true' width=\"80\">](https://github.com/jeshraghian/snntorch/)\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "id": "4blpfg4y44uO"
   },
   "source": [
    "This tutorial is based on the following papers on nonlinear regression and spiking neural networks. If you find these resources or code useful in your work, please consider citing the following sources:\n",
    "\n",
    "> <cite> [Alexander Henkes, Jason K. Eshraghian, and Henning Wessels. “Spiking neural networks for nonlinear regression\", arXiv preprint arXiv:2210.03515, October 2022.](https://arxiv.org/abs/2210.03515) </cite>\n",
    "\n",
    "> <cite> [Jason K. Eshraghian, Max Ward, Emre Neftci, Xinxin Wang, Gregor Lenz, Girish Dwivedi, Mohammed Bennamoun, Doo Seok Jeong, and Wei D. Lu. \"Training Spiking Neural Networks Using Lessons From Deep Learning\". Proceedings of the IEEE, 111(9) September 2023.](https://ieeexplore.ieee.org/abstract/document/10242251) </cite>"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "id": "lnF_PEo5obYv",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "In the regression tutorial series, you will learn how to use snnTorch to perform regression using a variety of spiking neuron models, including:\n",
    "\n",
    "* Leaky Integrate-and-Fire (LIF) Neurons\n",
    "* Recurrent LIF Neurons\n",
    "* Spiking LSTMs\n",
    "\n",
    "An overview of the regression tutorial series:\n",
    "\n",
    "* Part I (this tutorial) will train the membrane potential of a LIF neuron to follow a given trajectory over time.\n",
    "* Part II will use LIF neurons with recurrent feedback to perform classification using regression-based loss functions\n",
    "* Part III will use a more complex spiking LSTM network instead to train the firing time of a neuron.\n",
    "\n",
    "\n",
    "If running in Google Colab:\n",
    "* You may connect to GPU by checking `Runtime` > `Change runtime type` > `Hardware accelerator: GPU`\n",
    "* Next, install the latest PyPi distribution of snnTorch and Tonic by clicking into the following cell and pressing `Shift+Enter`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install snntorch --quiet"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# imports\n",
    "import snntorch as snn\n",
    "from snntorch import surrogate\n",
    "from snntorch import functional as SF\n",
    "from snntorch import utils\n",
    "\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "from torch.utils.data import DataLoader\n",
    "from torchvision import datasets, transforms\n",
    "import torch.nn.functional as F\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import itertools\n",
    "import random\n",
    "import statistics\n",
    "import tqdm"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fix the random seed:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Seed\n",
    "torch.manual_seed(0)\n",
    "random.seed(0)\n",
    "np.random.seed(0)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 1. Spiking Regression\n",
    "## 1.1 A Quick Background on Linear and Nonlinear Regression"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The tutorials so far have focused on multi-class classification problems. But if you've made it this far, then it's probably safe to say that your brain can do more than distinguish cats and dogs. You're amazing and we believe in you.\n",
    "\n",
    "An alternative problem is regression, where multiple input features $x_i$ are used to estimate an output on a continuous number line $y \\in \\mathbb{R}$. \n",
    "A classic example is estimating the price of a house, given a bunch of inputs such as land size, number of rooms, and the local demand for avocado toast.\n",
    "\n",
    "The objective of a regression problem is often the mean-square error:\n",
    "\n",
    "$$\\mathcal{L}_{MSE} = \\frac{1}{n}\\sum_{i=1}^n(y_i-\\hat{y_i})^2$$\n",
    "\n",
    "or the mean absolute error:\n",
    "\n",
    "$$\\mathcal{L}_{L1} = \\frac{1}{n}\\sum_{i=1}^n|y_i-\\hat{y_i}|$$\n",
    "\n",
    "\n",
    "where $y$ is the target and $\\hat{y}$ is the predicted value. \n",
    "\n",
    "One of the challenges of linear regression is that it can only use linear weightings of input features in predicting the output. \n",
    "Using a neural network trained using the mean-square error as the cost function allows us to perform nonlinear regression on more complex data. "
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1.2 Spiking Neurons in Regression\n",
    "\n",
    "Spikes are a type of nonlinearity that can also be used to learn more complex regression tasks. \n",
    "But if spiking neurons only emit spikes that are represented with 1's and 0's, then how might we perform regression? I'm glad you asked! Here are a few ideas:\n",
    "\n",
    "* Use the total number of spikes (a rate-based code)\n",
    "* Use the time of the spike (a temporal/latency-based code)\n",
    "* Use the distance between pairs of spikes (i.e., using the interspike interval)\n",
    "\n",
    "Or perhaps you pierce the neuron membrane with an electrical probe and decide to use the membrane potential instead, which is a continuous value. \n",
    "\n",
    "> Note: is it cheating to directly access the membrane potential, i.e., something that is meant to be a 'hidden state'? At this time, there isn't much consensus in the neuromorphic community. Despite being a high precision variable in many models (and thus computationally expensive), the membrane potential is commonly used in loss functions as it is a more 'continuous' variable compared to discrete time steps or spike counts. While it costs more in terms of power and latency to operate on higher-precision values, the impact might be minor if you have a small output layer, or if the output does not need to be scaled by weights. It really is a task-specific and hardware-specific question. "
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 2. Setting up the Regression Problem\n",
    "\n",
    "## 2.1 Create Dataset\n",
    "\n",
    "Let's construct a simple toy problem. The following class returns the function we are hoping to learn. If `mode = \"linear\"`, a straight line with a random slope is generated. If `mode = \"sqrt\"`, then the square root of this straight line is taken instead. \n",
    "\n",
    "Our goal: train a leaky integrate-and-fire neuron such that its membrane potential follows the sample over time. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class RegressionDataset(torch.utils.data.Dataset):\n",
    "    \"\"\"Simple regression dataset.\"\"\"\n",
    "\n",
    "    def __init__(self, timesteps, num_samples, mode):\n",
    "        \"\"\"Linear relation between input and output\"\"\"\n",
    "        self.num_samples = num_samples # number of generated samples\n",
    "        feature_lst = [] # store each generated sample in a list\n",
    "\n",
    "        # generate linear functions one by one\n",
    "        for idx in range(num_samples):\n",
    "            end = float(torch.rand(1)) # random final point\n",
    "            lin_vec = torch.linspace(start=0.0, end=end, steps=timesteps) # generate linear function from 0 to end\n",
    "            feature = lin_vec.view(timesteps, 1)\n",
    "            feature_lst.append(feature) # add sample to list\n",
    "\n",
    "        self.features = torch.stack(feature_lst, dim=1) # convert list to tensor\n",
    "\n",
    "        # option to generate linear function or square-root function\n",
    "        if mode == \"linear\":\n",
    "            self.labels = self.features * 1\n",
    "\n",
    "        elif mode == \"sqrt\":\n",
    "            slope = float(torch.rand(1))\n",
    "            self.labels = torch.sqrt(self.features * slope)\n",
    "\n",
    "        else:\n",
    "            raise NotImplementedError(\"'linear', 'sqrt'\")\n",
    "\n",
    "    def __len__(self):\n",
    "        \"\"\"Number of samples.\"\"\"\n",
    "        return self.num_samples\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        \"\"\"General implementation, but we only have one sample.\"\"\"\n",
    "        return self.features[:, idx, :], self.labels[:, idx, :]\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To see what a random sample looks like, run the following code-block:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "num_steps = 50\n",
    "num_samples = 1\n",
    "mode = \"sqrt\" # 'linear' or 'sqrt'\n",
    "\n",
    "# generate a single data sample\n",
    "dataset = RegressionDataset(timesteps=num_steps, num_samples=num_samples, mode=mode)\n",
    "\n",
    "# plot\n",
    "sample = dataset.labels[:, 0, 0]\n",
    "plt.plot(sample)\n",
    "plt.title(\"Target function to teach network\")\n",
    "plt.xlabel(\"Time\")\n",
    "plt.ylabel(\"Membrane Potential\")\n",
    "plt.show()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2.2 Create DataLoader\n",
    "\n",
    "The Dataset objects created above load data into memory, and the DataLoader will serve it up in batches. DataLoaders in PyTorch are a handy interface for passing data into a network. They return an iterator divided up into mini-batches of size ``batch_size``."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "batch_size = 1 # only one sample to learn\n",
    "dataloader = torch.utils.data.DataLoader(dataset=dataset, batch_size=batch_size, drop_last=True)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 3. Construct Model\n",
    "\n",
    "Let us try a simple network using only leaky integrate-and-fire layers without recurrence. \n",
    "Subsequent tutorials will show how to use more complex neuron types with higher-order recurrence.\n",
    "These architectures should work just fine, if there is no strong time dependency in the data, i.e., the next time step has weak dependence on the previous one.\n",
    "\n",
    "A few notes on the architecture below:\n",
    "\n",
    "* Setting `learn_beta=True` enables the decay rate `beta` to be a learnable parameter\n",
    "* Each neuron has a unique, and randomly initialized threshold and decay rate\n",
    "* The output layer has the reset mechanism disabled by setting `reset_mechanism=\"none\"` as we will not use any output spikes"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Net(torch.nn.Module):\n",
    "    \"\"\"Simple spiking neural network in snntorch.\"\"\"\n",
    "\n",
    "    def __init__(self, timesteps, hidden):\n",
    "        super().__init__()\n",
    "        \n",
    "        self.timesteps = timesteps # number of time steps to simulate the network\n",
    "        self.hidden = hidden # number of hidden neurons \n",
    "        spike_grad = surrogate.fast_sigmoid() # surrogate gradient function\n",
    "        \n",
    "        # randomly initialize decay rate and threshold for layer 1\n",
    "        beta_in = torch.rand(self.hidden)\n",
    "        thr_in = torch.rand(self.hidden)\n",
    "\n",
    "        # layer 1\n",
    "        self.fc_in = torch.nn.Linear(in_features=1, out_features=self.hidden)\n",
    "        self.lif_in = snn.Leaky(beta=beta_in, threshold=thr_in, learn_beta=True, spike_grad=spike_grad)\n",
    "        \n",
    "        # randomly initialize decay rate and threshold for layer 2\n",
    "        beta_hidden = torch.rand(self.hidden)\n",
    "        thr_hidden = torch.rand(self.hidden)\n",
    "\n",
    "        # layer 2\n",
    "        self.fc_hidden = torch.nn.Linear(in_features=self.hidden, out_features=self.hidden)\n",
    "        self.lif_hidden = snn.Leaky(beta=beta_hidden, threshold=thr_hidden, learn_beta=True, spike_grad=spike_grad)\n",
    "\n",
    "        # randomly initialize decay rate for output neuron\n",
    "        beta_out = torch.rand(1)\n",
    "        \n",
    "        # layer 3: leaky integrator neuron. Note the reset mechanism is disabled and we will disregard output spikes.\n",
    "        self.fc_out = torch.nn.Linear(in_features=self.hidden, out_features=1)\n",
    "        self.li_out = snn.Leaky(beta=beta_out, threshold=1.0, learn_beta=True, spike_grad=spike_grad, reset_mechanism=\"none\")\n",
    "\n",
    "    def forward(self, x):\n",
    "        \"\"\"Forward pass for several time steps.\"\"\"\n",
    "\n",
    "        # Initalize membrane potential\n",
    "        mem_1 = self.lif_in.init_leaky()\n",
    "        mem_2 = self.lif_hidden.init_leaky()\n",
    "        mem_3 = self.li_out.init_leaky()\n",
    "\n",
    "        # Empty lists to record outputs\n",
    "        mem_3_rec = []\n",
    "\n",
    "        # Loop over \n",
    "        for step in range(self.timesteps):\n",
    "            x_timestep = x[step, :, :]\n",
    "\n",
    "            cur_in = self.fc_in(x_timestep)\n",
    "            spk_in, mem_1 = self.lif_in(cur_in, mem_1)\n",
    "            \n",
    "            cur_hidden = self.fc_hidden(spk_in)\n",
    "            spk_hidden, mem_2 = self.li_out(cur_hidden, mem_2)\n",
    "\n",
    "            cur_out = self.fc_out(spk_hidden)\n",
    "            _, mem_3 = self.li_out(cur_out, mem_3)\n",
    "\n",
    "            mem_3_rec.append(mem_3)\n",
    "\n",
    "        return torch.stack(mem_3_rec)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Instantiate the network below:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "hidden = 128\n",
    "device = torch.device(\"cuda\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "model = Net(timesteps=num_steps, hidden=hidden).to(device)\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's observe the behavior of the output neuron before it has been trained and how it compares to the target function:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_batch = iter(dataloader)\n",
    "\n",
    "# run a single forward-pass\n",
    "with torch.no_grad():\n",
    "    for feature, label in train_batch:\n",
    "        feature = torch.swapaxes(input=feature, axis0=0, axis1=1)\n",
    "        label = torch.swapaxes(input=label, axis0=0, axis1=1)\n",
    "        feature = feature.to(device)\n",
    "        label = label.to(device)\n",
    "        mem = model(feature)\n",
    "\n",
    "# plot\n",
    "plt.plot(mem[:, 0, 0].cpu(), label=\"Output\")\n",
    "plt.plot(label[:, 0, 0].cpu(), '--', label=\"Target\")\n",
    "plt.title(\"Untrained Output Neuron\")\n",
    "plt.xlabel(\"Time\")\n",
    "plt.ylabel(\"Membrane Potential\")\n",
    "plt.legend(loc='best')\n",
    "plt.show()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As the network has not yet been trained, it is unsurprising the membrane potential follows a senseless evolution."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 4. Construct Training Loop\n",
    "\n",
    "We call `torch.nn.MSELoss()` to minimize the mean square error between the membrane potential and the target evolution.\n",
    "\n",
    "We iterate over the same sample of data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "num_iter = 100 # train for 100 iterations\n",
    "optimizer = torch.optim.Adam(params=model.parameters(), lr=1e-3)\n",
    "loss_function = torch.nn.MSELoss()\n",
    "\n",
    "loss_hist = [] # record loss\n",
    "\n",
    "# training loop\n",
    "with tqdm.trange(num_iter) as pbar:\n",
    "    for _ in pbar:\n",
    "        train_batch = iter(dataloader)\n",
    "        minibatch_counter = 0\n",
    "        loss_epoch = []\n",
    "        \n",
    "        for feature, label in train_batch:\n",
    "            # prepare data\n",
    "            feature = torch.swapaxes(input=feature, axis0=0, axis1=1)\n",
    "            label = torch.swapaxes(input=label, axis0=0, axis1=1)\n",
    "            feature = feature.to(device)\n",
    "            label = label.to(device)\n",
    "\n",
    "            # forward pass\n",
    "            mem = model(feature)\n",
    "            loss_val = loss_function(mem, label) # calculate loss\n",
    "            optimizer.zero_grad() # zero out gradients\n",
    "            loss_val.backward() # calculate gradients\n",
    "            optimizer.step() # update weights\n",
    "\n",
    "            # store loss\n",
    "            loss_hist.append(loss_val.item())\n",
    "            loss_epoch.append(loss_val.item())\n",
    "            minibatch_counter += 1\n",
    "\n",
    "            avg_batch_loss = sum(loss_epoch) / minibatch_counter # calculate average loss p/epoch\n",
    "            pbar.set_postfix(loss=\"%.3e\" % avg_batch_loss) # print loss p/batch"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 5. Evaluation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "loss_function = torch.nn.L1Loss() # Use L1 loss instead\n",
    "\n",
    " # pause gradient calculation during evaluation\n",
    "with torch.no_grad():\n",
    "    model.eval()\n",
    "\n",
    "    test_batch = iter(dataloader)\n",
    "    minibatch_counter = 0\n",
    "    rel_err_lst = []\n",
    "\n",
    "    # loop over data samples\n",
    "    for feature, label in test_batch:\n",
    "\n",
    "        # prepare data\n",
    "        feature = torch.swapaxes(input=feature, axis0=0, axis1=1)\n",
    "        label = torch.swapaxes(input=label, axis0=0, axis1=1)\n",
    "        feature = feature.to(device)\n",
    "        label = label.to(device)\n",
    "\n",
    "        # forward-pass\n",
    "        mem = model(feature)\n",
    "\n",
    "        # calculate relative error\n",
    "        rel_err = torch.linalg.norm(\n",
    "            (mem - label), dim=-1\n",
    "        ) / torch.linalg.norm(label, dim=-1)\n",
    "        rel_err = torch.mean(rel_err[1:, :])\n",
    "\n",
    "        # calculate loss\n",
    "        loss_val = loss_function(mem, label)\n",
    "\n",
    "        # store loss\n",
    "        loss_hist.append(loss_val.item())\n",
    "        rel_err_lst.append(rel_err.item())\n",
    "        minibatch_counter += 1\n",
    "\n",
    "    mean_L1 = statistics.mean(loss_hist)\n",
    "    mean_rel = statistics.mean(rel_err_lst)\n",
    "\n",
    "print(f\"{'Mean L1-loss:':<{20}}{mean_L1:1.2e}\")\n",
    "print(f\"{'Mean rel. err.:':<{20}}{mean_rel:1.2e}\")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's plot our results for some visual intuition:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mem = mem.cpu()\n",
    "label = label.cpu()\n",
    "\n",
    "plt.title(\"Trained Output Neuron\")\n",
    "plt.xlabel(\"Time\")\n",
    "plt.ylabel(\"Membrane Potential\")\n",
    "for i in range(batch_size):\n",
    "    plt.plot(mem[:, i, :].cpu(), label=\"Output\")\n",
    "    plt.plot(label[:, i, :].cpu(), label=\"Target\")\n",
    "plt.legend(loc='best')\n",
    "plt.show()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It is a little jagged, but it's not looking too bad. \n",
    "\n",
    "You might try to improve the curve fit by expanding the size of the hidden layer, increasing the number of iterations, adding extra time steps, hyperparameter fine-tuning, or using a completely different neuron type."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Conclusion\n",
    "\n",
    "The next regression tutorials will test more powerful spiking neurons, such as Recurrent LIF neurons and spiking LSTMs, to see how they compare.\n",
    "\n",
    "If you like this project, please consider starring ⭐ the repo on GitHub as it is the easiest and best way to support it.\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Additional Resources\n",
    "* [Check out the snnTorch GitHub project here.](https://github.com/jeshraghian/snntorch)\n",
    "* More detail on nonlinear regression with SNNs can be found in our corresponding preprint here: [Henkes, A.; Eshraghian, J. K.; and Wessels, H.  “Spiking neural networks for nonlinear regression\", arXiv preprint arXiv:2210.03515, Oct. 2022.](https://arxiv.org/abs/2210.03515) "
   ]
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "celltoolbar": "Raw Cell Format",
  "colab": {
   "collapsed_sections": [
    "9QXsrr6Mp5e_",
    "1EWDw3bip8Ie",
    "vFM8UV9CreIX",
    "xXkTAJ9ws1Y6",
    "OgkWg605tE1y",
    "OBt0WDzyujnk",
    "xC96eesMqYo-",
    "mszPTrYOluym",
    "VTHK-wAWV57B"
   ],
   "name": "snntorch_regression_1.ipynb",
   "provenance": []
  },
  "kernelspec": {
   "display_name": "torch-gpu",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.15"
  },
  "vscode": {
   "interpreter": {
    "hash": "2a3056c17c3c31a88ffeb08a28ff32bf922ba3f6fa0343ca62cc95241e30c809"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
